package dynamic.ibatis.executor.loader;

/*
 *    Copyright 2009-2023 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       https://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */


import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import dynamic.ibatis.executor.BatchResult;
import dynamic.ibatis.executor.DBaseExecutor;
import dynamic.ibatis.mapping.DBoundSql;
import dynamic.ibatis.mapping.DMappedStatement;
import dynamic.ibatis.reflection.MetaObject;
import dynamic.ibatis.session.DConfiguration;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.ExecutorException;

import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;

import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

/**
 * @author Clinton Begin
 * @author Franta Mejta
 */
public class ResultLoaderMap {

    private final Map<String, ResultLoaderMap.LoadPair> loaderMap = new HashMap<>();

    public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
        String upperFirst = getUppercaseFirstProperty(property);
        if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
            throw new ExecutorException("Nested lazy loaded result property '" + property + "' for query id '"
                    + resultLoader.mappedStatement.getId()
                    + " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
        }
        loaderMap.put(upperFirst, new ResultLoaderMap.LoadPair(property, metaResultObject, resultLoader));
    }

    public final Map<String, ResultLoaderMap.LoadPair> getProperties() {
        return new HashMap<>(this.loaderMap);
    }

    public Set<String> getPropertyNames() {
        return loaderMap.keySet();
    }

    public int size() {
        return loaderMap.size();
    }

    public boolean hasLoader(String property) {
        return loaderMap.containsKey(property.toUpperCase(Locale.ENGLISH));
    }

    public boolean load(String property) throws SQLException {
        ResultLoaderMap.LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
        if (pair != null) {
            pair.load();
            return true;
        }
        return false;
    }

    public void remove(String property) {
        loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
    }

    public void loadAll() throws SQLException {
        final Set<String> methodNameSet = loaderMap.keySet();
        String[] methodNames = methodNameSet.toArray(new String[methodNameSet.size()]);
        for (String methodName : methodNames) {
            load(methodName);
        }
    }

    private static String getUppercaseFirstProperty(String property) {
        String[] parts = property.split("\\.");
        return parts[0].toUpperCase(Locale.ENGLISH);
    }

    /**
     * Property which was not loaded yet.
     */
    public static class LoadPair implements Serializable {

        private static final long serialVersionUID = 20130412;
        /**
         * Name of factory method which returns database connection.
         */
        private static final String FACTORY_METHOD = "getConfiguration";
        /**
         * Object to check whether we went through serialization..
         */
        private final transient Object serializationCheck = new Object();
        /**
         * Meta object which sets loaded properties.
         */
        private transient MetaObject metaResultObject;
        /**
         * Result loader which loads unread properties.
         */
        private transient ResultLoader resultLoader;
        /**
         * Wow, logger.
         */
        private transient Log log;
        /**
         * Factory class through which we get database connection.
         */
        private Class<?> configurationFactory;
        /**
         * Name of the unread property.
         */
        private final String property;
        /**
         * ID of SQL statement which loads the property.
         */
        private String mappedStatement;
        /**
         * Parameter of the sql statement.
         */
        private Serializable mappedParameter;

        private LoadPair(final String property, MetaObject metaResultObject,ResultLoader resultLoader) {
            this.property = property;
            this.metaResultObject = metaResultObject;
            this.resultLoader = resultLoader;

            /* Save required information only if original object can be serialized. */
            if (metaResultObject != null && metaResultObject.getOriginalObject() instanceof Serializable) {
                final Object mappedStatementParameter = resultLoader.parameterObject;

                /* @todo May the parameter be null? */
                if (mappedStatementParameter instanceof Serializable) {
                    this.mappedStatement = resultLoader.mappedStatement.getId();
                    this.mappedParameter = (Serializable) mappedStatementParameter;

                    this.configurationFactory = resultLoader.configuration.getConfigurationFactory();
                } else {
                    Log log = this.getLogger();
                    if (log.isDebugEnabled()) {
                        log.debug("Property [" + this.property + "] of [" + metaResultObject.getOriginalObject().getClass()
                                + "] cannot be loaded " + "after deserialization. Make sure it's loaded before serializing "
                                + "forenamed object.");
                    }
                }
            }
        }

        public void load() throws SQLException {
            /*
             * These field should not be null unless the loadpair was serialized. Yet in that case this method should not be
             * called.
             */
            if (this.metaResultObject == null) {
                throw new IllegalArgumentException("metaResultObject is null");
            }
            if (this.resultLoader == null) {
                throw new IllegalArgumentException("resultLoader is null");
            }

            this.load(null);
        }

        public void load(final Object userObject) throws SQLException {
            if (this.metaResultObject == null || this.resultLoader == null) {
                if (this.mappedParameter == null) {
                    throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
                            + "required parameter of mapped statement [" + this.mappedStatement + "] is not serializable.");
                }

                final DConfiguration config = this.getConfiguration();
                final DMappedStatement ms = config.getMappedStatement(this.mappedStatement);
                if (ms == null) {
                    throw new ExecutorException(
                            "Cannot lazy load property [" + this.property + "] of deserialized object [" + userObject.getClass()
                                    + "] because configuration does not contain statement [" + this.mappedStatement + "]");
                }

                this.metaResultObject = config.newMetaObject(userObject);
                this.resultLoader = new ResultLoader(config, new ResultLoaderMap.ClosedExecutor(), ms, this.mappedParameter,
                        metaResultObject.getSetterType(this.property), null, null);
            }

            /*
             * We are using a new executor because we may be (and likely are) on a new thread and executors aren't thread
             * safe. (Is this sufficient?) A better approach would be making executors thread safe.
             */
            if (this.serializationCheck == null) {
                final ResultLoader old = this.resultLoader;
                this.resultLoader = new ResultLoader(old.configuration, new ResultLoaderMap.ClosedExecutor(), old.mappedStatement,
                        old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
            }

            this.metaResultObject.setValue(property, this.resultLoader.loadResult());
        }

        private DConfiguration getConfiguration() {
            if (this.configurationFactory == null) {
                throw new ExecutorException("Cannot get Configuration as configuration factory was not set.");
            }

            Object configurationObject;
            try {
                final Method factoryMethod = this.configurationFactory.getDeclaredMethod(FACTORY_METHOD);
                if (!Modifier.isStatic(factoryMethod.getModifiers())) {
                    throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
                            + FACTORY_METHOD + "] is not static.");
                }

                if (!factoryMethod.isAccessible()) {
                    configurationObject = AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {
                        try {
                            factoryMethod.setAccessible(true);
                            return factoryMethod.invoke(null);
                        } finally {
                            factoryMethod.setAccessible(false);
                        }
                    });
                } else {
                    configurationObject = factoryMethod.invoke(null);
                }
            } catch (final ExecutorException ex) {
                throw ex;
            } catch (final NoSuchMethodException ex) {
                throw new ExecutorException("Cannot get Configuration as factory class [" + this.configurationFactory
                        + "] is missing factory method of name [" + FACTORY_METHOD + "].", ex);
            } catch (final PrivilegedActionException ex) {
                throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
                        + FACTORY_METHOD + "] threw an exception.", ex.getCause());
            } catch (final Exception ex) {
                throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
                        + FACTORY_METHOD + "] threw an exception.", ex);
            }

            if (!(configurationObject instanceof Configuration)) {
                throw new ExecutorException("Cannot get Configuration as factory method [" + this.configurationFactory + "]#["
                        + FACTORY_METHOD + "] didn't return [" + Configuration.class + "] but ["
                        + (configurationObject == null ? "null" : configurationObject.getClass()) + "].");
            }

            return DConfiguration.class.cast(configurationObject);
        }

        private Log getLogger() {
            if (this.log == null) {
                this.log = LogFactory.getLog(this.getClass());
            }
            return this.log;
        }
    }

    private static final class ClosedExecutor extends DBaseExecutor {

        public ClosedExecutor() {
            super(null, null);
        }

        @Override
        public boolean isClosed() {
            return true;
        }

        @Override
        protected int doUpdate(DMappedStatement ms, Object parameter) throws SQLException {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        protected List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        protected <E> List<E> doQuery(DMappedStatement ms, Object parameter, RowBounds rowBounds,
                                      ResultHandler resultHandler, DBoundSql boundSql) throws SQLException {
            throw new UnsupportedOperationException("Not supported.");
        }

        @Override
        protected <E> Cursor<E> doQueryCursor(DMappedStatement ms, Object parameter, RowBounds rowBounds, DBoundSql boundSql)
                throws SQLException {
            throw new UnsupportedOperationException("Not supported.");
        }
    }
}
